package edu.ubc.javascript;


import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.List;

import com.google.common.base.Preconditions;
import com.google.javascript.jscomp.mozilla.rhino.Node;
import com.google.javascript.jscomp.mozilla.rhino.ast.AstNode;
import com.google.javascript.jscomp.mozilla.rhino.ast.AstRoot;
import com.google.javascript.jscomp.mozilla.rhino.ast.Block;
import com.google.javascript.jscomp.mozilla.rhino.ast.FunctionNode;
import com.google.javascript.jscomp.mozilla.rhino.ast.SwitchCase;

public class ReflectiveAstTransformer {
	
	private List<Runnable> operations = new LinkedList<Runnable>();
	
	public void commit() {
		for(Runnable runner : operations) {
			try {
				runner.run();
			} catch(Exception e) {
				e.printStackTrace();
			}
		}
		operations.clear();
	}
	
	private static class ReplaceFieldOperation implements Runnable {

		private Field field;
		private AstNode parent;
		private AstNode newer;
		
		public ReplaceFieldOperation(Field field, AstNode parent, AstNode newer) {
			this.field = field;
			this.parent = parent;
			this.newer = newer;
		}
		
		@Override
		public void run() {
			try {
				Node older = (Node) field.get(parent);
				if((parent instanceof Block) || (parent instanceof AstRoot)) {
					parent.replaceChild(older, newer);
				} else {
					field.set(parent, newer);
				}
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
		
	}
	
	
	private static class ReplaceElementOperation implements Runnable {
		private List otherList;
		private int index;
		private AstNode newer;
		
		public ReplaceElementOperation(List otherList, int index, AstNode newer) {
			this.otherList = otherList;
			this.index = index;
			this.newer = newer;
		}
		
		@Override
		public void run() {
			otherList.remove(index);
			otherList.add(index, newer);
		}
		
	}
	
	private static class RemoveFieldOperation implements Runnable {

		private Field field;
		private AstNode parent;
		
		public RemoveFieldOperation(Field field, AstNode parent) {
			this.field = field;
			this.parent = parent;
		}
		
		@Override
		public void run() {
			try {
				field.set(parent, null);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
		
	}
	
	private static class RemoveLinkedFieldOperation implements Runnable {

		private Node other;
		private AstNode parent;
		
		public RemoveLinkedFieldOperation(Node other, AstNode parent) {
			this.other = other;
			this.parent = parent;
		}
		
		@Override
		public void run() {
			try {
				parent.removeChild(other);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
		
	}
	
	
	private static class RemoveElementOperation implements Runnable {
		private List otherList;
		private int index;
		
		public RemoveElementOperation(List otherList, int index) {
			this.otherList = otherList;
			this.index = index;
		}
		
		@Override
		public void run() {
			otherList.remove(index);
		}
		
	}
	
	private static class InsertBeforeLinkedFieldOperation implements Runnable {
		Node block;
		Node current;
		Node newer;
		
		public InsertBeforeLinkedFieldOperation(Node block, Node current, Node newer) {
			this.block = block;
			this.current = current;
			this.newer = newer;
		}

		@Override
		public void run() {
			try {
				if(block instanceof SwitchCase) {
					SwitchCase sc = (SwitchCase) block;
					int index = sc.getStatements().indexOf(current);
					sc.getStatements().add(index, (AstNode) newer);
				} else {
					block.addChildBefore(newer, current);
				}
			} catch(Exception e) {
				throw new RuntimeException(e);
			}
		}
		
	}
	
	private static class InsertBeforeListOperation implements Runnable {
		Node block;
		Field f;
		Node current;
		Node newer;
		
		public InsertBeforeListOperation(Node block, Field f, Node current, Node newer) {
			this.block = block;
			this.f = f;
			this.current = current;
			this.newer = newer;
		}

		@Override
		public void run() {
			try {
				f.setAccessible(true);
				List stmts = (List) f.get(block);
				int index = stmts.indexOf(current);
				stmts.add(index, newer);
			} catch(Exception e) {
				throw new RuntimeException(e);
			}
		}
		
	}
	
	private static class InsertAfterListOperation implements Runnable {
		Node block;
		Field f;
		Node current;
		Node newer;
		
		public InsertAfterListOperation(Node block, Field f, Node current, Node newer) {
			this.block = block;
			this.f = f;
			this.current = current;
			this.newer = newer;
		}

		@Override
		public void run() {
			try {
				f.setAccessible(true);
				List stmts = (List) f.get(block);
				int index = stmts.indexOf(current);
				stmts.add(index+1, newer);
			} catch(Exception e) {
				throw new RuntimeException(e);
			}
		}
		
	}
	
	private static class InsertAfterLinkedFieldOperation implements Runnable {
		Node block;
		Node current;
		Node newer;
		
		public InsertAfterLinkedFieldOperation(Node block2, Node current, Node newer) {
			this.block = block2;
			if(current == null) {
				System.out.println();
			}
			this.current = current;
			this.newer = newer;
		}

		@Override
		public void run() {
			try {
				block.addChildAfter(newer, current);
			} catch(Exception e) {
				throw new RuntimeException(e);
			}
		}
	}
	
	public void afterExecution(FunctionNode fn, List<Node> newer) {
		for(Node newNode : newer) {
			afterExecution(fn, newNode);
		}
	}
	
	public void afterExecution(FunctionNode fn, Node newer) {
		Block block = (Block) fn.getBody();
		insertAfter(block, block.getLastChild(), newer); 
	}
	
	public void insertAfter(Node block, Node current, List<Node> newer) {
		for(Node newNode : newer) {
			insertAfter(block, current, newNode);
		}
	}
	
	public void beforeExecution(FunctionNode fn, List<AstNode> list) {
		for(Node newNode : list) {
			beforeExecution(fn, newNode);
		}
	}
	
	public void beforeExecution(FunctionNode fn, Node newer) {
		Block block = (Block) fn.getBody();
		insertBefore(block, block.getFirstChild(), newer); 
	}
	
	public void insertBefore(Node block, Node current, List<Node> newer) {
		for(Node newNode : newer) {
			insertBefore(block, current, newNode);
		}
	}
	
	public void insertBefore(final Node block, Node current, Node newer) {
		if((block instanceof Block) || (block instanceof SwitchCase) || (block instanceof AstRoot)) {
			operations.add(new InsertBeforeLinkedFieldOperation(block, current, newer));
		} else {
			Field f;
			try {
				f = block.getClass().getDeclaredField("statements");
				operations.add(new InsertBeforeListOperation(block, f, current, newer));
			} catch (Exception e) {
				try {
					final Result r = findInClassHierarchy(block.getClass(), block, current);
					Node found = (Node) r.field.get(block);
					final Block b = new Block();
					b.addChild((AstNode) found);
					b.addChild((AstNode) newer);
					operations.add(new Runnable() {
						public void run() {
							try {
								r.field.set(block, b);
							} catch (Exception e) {
								throw new RuntimeException(e);
							}
						}
					});
				} catch (Exception e1) {
					throw new RuntimeException(e1);
				}
			}
		}
	}
	
	public void insertAfter(final Node block, Node current, Node newer) {
		if((block instanceof Block) || (block instanceof SwitchCase) || (block instanceof AstRoot)) {
			operations.add(new InsertAfterLinkedFieldOperation(block, current, newer));
		} else {
			Field f;
			try {
				f = block.getClass().getDeclaredField("statements");
				operations.add(new InsertAfterListOperation(block, f, current, newer));
			} catch (Exception e) {
				try {
					final Result r = findInClassHierarchy(block.getClass(), block, current);
					Node found = (Node) r.field.get(block);
					final Block b = new Block();
					b.addChild((AstNode) newer);
					b.addChild((AstNode) found);
					operations.add(new Runnable() {
						public void run() {
							try {
								r.field.set(block, b);
							} catch (Exception e) {
								throw new RuntimeException(e);
							}
						}
					});
				} catch (Exception e1) {
					throw new RuntimeException(e1);
				}
			}
		}
	}
	
	public boolean removeNode(AstNode parent, AstNode older) {
		try {
			Result result = findInClassHierarchy(parent.getClass(), parent, older);
			if(result != null) {
				if(List.class.isAssignableFrom(result.field.getType())) {
					List otherList = (List) result.field.get(parent);
					int index = otherList.indexOf(older);
					operations.add(new RemoveElementOperation(otherList, index));
				} else if(!result.linked) {
					operations.add(new RemoveFieldOperation(result.field, parent));
				} else {
					operations.add(new RemoveLinkedFieldOperation(older, parent));
				}
				return true;
			} else {
				return false;
			}
		} catch(Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	public boolean replaceNode(AstNode parent, AstNode older, List<AstNode> newer) {
		if(newer.size() == 1) {
			replaceNode(parent, older, newer.get(0));
		} else {
			for(int i=0;i < newer.size();i++) {
				if(i == newer.size() - 1) {
					replaceNode(parent, older, newer.get(i));
				} else {
					insertBefore(parent, older, newer.get(i));
				}
			}
		}
		return true;
	}
	
	public boolean replaceNode(final AstNode parent, final AstNode older, final AstNode newer) {
		try {
			Preconditions.checkArgument(newer.getParent() == null);
			Result result = findInClassHierarchy(parent.getClass(), parent, older);
			final Field field = result.field;
			if(field != null) {
				if(List.class.isAssignableFrom(field.getType())) {
					List otherList = (List) field.get(parent);
					int index = otherList.indexOf(older);
					operations.add(new ReplaceElementOperation(otherList, index, newer));
				} else if(result.linked) {
					operations.add(new Runnable() {
						public void run() {
							try {
								parent.replaceChild(older, newer);
							} catch (Exception e) {
								throw new RuntimeException(e);
							}
						}
					});
				} else {
					operations.add(new ReplaceFieldOperation(field, parent, newer));
				}
				return true;
			} else {
				return false;
			}
		} catch(Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	private Result findInClassHierarchy(Class clazz, Node parent, Node older) throws IllegalArgumentException, IllegalAccessException {
		Result found = findInClass(clazz, parent, older);
		if(found != null) {
			return found;
		} else if(clazz.getSuperclass() != null) {
			Class superClass = clazz.getSuperclass();
			return findInClassHierarchy(superClass, parent, older);
		} else {
			return null;
		}
	}
	
	private Result findInClass(Class clazz, Node parent, Node older) throws IllegalArgumentException, IllegalAccessException {
		Field[] fields = clazz.getDeclaredFields();
		for(Field field : fields) {
			Class fieldTypeClass = field.getType();
			if(AstNode.class.isAssignableFrom(fieldTypeClass)) {
				field.setAccessible(true);
				AstNode other = (AstNode) field.get(parent);
				if(other == older) {
					return new Result(field, false);
				}
			} else if(List.class.isAssignableFrom(fieldTypeClass)) {
				field.setAccessible(true);
				Object listObject = field.get(parent);
				List otherList = (List) listObject;
				if(otherList != null) {
					int index = otherList.indexOf(older);
					if(index != -1) {
						return new Result(field, false);
					}
				}
			} else if(Node.class.isAssignableFrom(fieldTypeClass)) {
				if(field.getName().equals("first")) {
					field.setAccessible(true);
					Node other = (Node) field.get(parent);
					while(other != null) {
						if(other == older) {
							return new Result(field, true);
						}
						other = other.getNext();
					}
				}
			}
		}
		return null;
	}
	
	private static class Result {
		Field field;
		boolean linked;
		
		public Result(Field field, boolean linked) {
			this.field = field;
			this.linked = linked;
		}
		
	}

	
}
